from PyQt6.QtCore import *
from PyQt6.QtWidgets import *
from PyQt6.QtGui import *
from ui.ui import Ui_MainWindow
from ui.new_cmd_bt import Ui_new_cmd_button
from ui.new_tab_dia import Ui_new_tab_dia
import time
import re
import traceback
import serial.tools.list_ports
import configparser


class ComController(QMainWindow):
    def __init__(self):
        super(ComController, self).__init__()
        self.ui = Ui_MainWindow()
        self.ui.setupUi(self)
        self.showMaximized()
        self.ui.connect.setIcon(QIcon(r'.\images\widgets\connect.png'))
        self.ui.disconnect.setIcon(QIcon(r'.\images\widgets\disconnect_gray.png'))
        self.ui.disconnect.setEnabled(False)

        self.ser_scaner = SerialPortScan(ui_instance=self.ui)
        self.ser_scaner.daemon = True
        self.ser_scaner.homer.connect(self.dovecote)
        self.ser_timer = QTimer()
        self.ser_timer.timeout.connect(lambda: self.ser_scaner.run())
        self.ui.ser_ports_list.activated.connect(lambda: self.ser_timer.stop())
        self.ser_timer.start(2000)

        # toolBar
        # #  New Session button
        _btn_new = QAction('New Session', self)
        _btn_new.setStatusTip('This is a demo')
        _btn_new.setIcon(QIcon('.\images\_new.png'))
        _btn_new.setCheckable(True)
        _btn_new.triggered.connect(self.toolbar_action)
        self.ui.mytoolbar.addAction(_btn_new)
        # #  Delete Session button
        _btn_del = QAction('Delete Session', self)
        _btn_del.setStatusTip('This is a demo')
        _btn_del.setIcon(QIcon('.\images\_delete.png'))
        _btn_del.setCheckable(True)
        _btn_del.triggered.connect(self.toolbar_action)
        self.ui.mytoolbar.addAction(_btn_del)
        # #  Connect Session button
        _btn_connect = QAction('Connect', self)
        _btn_connect.setStatusTip('This is a demo')
        _btn_connect.setIcon(QIcon('.\images\_connect.png'))
        _btn_connect.setCheckable(True)
        _btn_connect.triggered.connect(self.toolbar_action)
        self.ui.mytoolbar.addAction(_btn_connect)
        # #  Disconnect Session button
        _btn_disconnect = QAction('Disconnect', self)
        _btn_disconnect.setStatusTip('This is a demo')
        _btn_disconnect.setIcon(QIcon('.\images\_disconnect.png'))
        _btn_disconnect.setCheckable(True)
        _btn_disconnect.triggered.connect(self.toolbar_action)
        self.ui.mytoolbar.addAction(_btn_disconnect)
        # #  Save log button
        _btn_save = QAction('Save current logs', self)
        _btn_save.setStatusTip('This is a demo')
        _btn_save.setIcon(QIcon('.\images\_save.png'))
        _btn_save.setCheckable(True)
        _btn_save.triggered.connect(self.toolbar_action)
        self.ui.mytoolbar.addAction(_btn_save)
        # #  Print log button
        _btn_setting = QAction('Settings', self)
        _btn_setting.setStatusTip('This is a demo')
        _btn_setting.setIcon(QIcon('.\images\_setting.png'))
        _btn_setting.setCheckable(True)
        _btn_setting.triggered.connect(self.toolbar_action)
        self.ui.mytoolbar.addAction(_btn_setting)

        # 串口
        self.ser, self.ser_flag = None, False
        self.ser_recv = None

        # MenuBar Operations
        self.cmd_name, self.cmd_payload = None, None
        self.nt_conform, self.nt_cancel = None, None
        self.clb_config = configparser.ConfigParser()
        self.current_index, self.current_widget = 0, None
        QApplication.instance().focusChanged.connect(self.get_focus_widget)
        self.ui.menuBar.currentChanged.connect(self.get_tab_id)
        self.new_tab_dia, self.new_bt_dia = NewTab(), NewBtDialog()
        # Create 32 layout, Max Tab  =  32
        self.qhbox = []
        [self.qhbox.append(QHBoxLayout()) for i in range(0, 31)]
        # add command button, addNewTab/addNewClb...are actions of customContextMenu
        # Menu
        self.context_menu = QMenu()
        self.add_tab_act = self.context_menu.addAction('AddNewTab')
        self.add_tab_act.setText('AddNewTab')
        self.remove_tab_act = self.context_menu.addAction('RemoveCurrentTab')
        self.remove_tab_act.setText('RemoveCurrentTab')
        self.add_clb_act = self.context_menu.addAction('AddNewCommandLinkButton')
        self.add_clb_act.setText('AddNewCommandLinkButton')
        self.remove_clb_act = self.context_menu.addAction('RemoveCurrentCommandLinkButton')
        self.remove_clb_act.setText('RemoveCurrentCommandLinkButton')
        self.modify_clb_act = self.context_menu.addAction('ModifyCommandLinkButton')
        self.modify_clb_act.setText('ModifyCommandLinkButton')
        self.context_menu.addActions([self.add_tab_act, self.remove_tab_act, self.add_clb_act,
                                      self.remove_clb_act, self.modify_clb_act])
        self.ui.menuBar.customContextMenuRequested.connect(lambda: self.context_menu.exec(QCursor.pos()))
        self.new_bt_dia.cmd_bt_conform.clicked.connect(lambda: self.new_clb())
        self.new_bt_dia.cmd_bt_cancel.clicked.connect(lambda: self.new_bt_dia.close())
        # add new Tab
        self.new_tab_dia.tab_name_cancel.clicked.connect(lambda: self.new_tab_dia.close())
        self.new_tab_dia.tab_name_conform.clicked.connect(lambda: self.new_tab())
        # load clb_list.ini when launch app
        self.load_clb_cfg()
        # def action
        self.add_tab_act.triggered.connect(lambda: self.new_tab_dia.exec())
        self.add_clb_act.triggered.connect(lambda: self.new_bt_dia.exec())
        self.remove_clb_act.triggered.connect(lambda: self.remove_clb_tab('clb'))
        self.remove_tab_act.triggered.connect(lambda: self.remove_clb_tab('tab'))
        self.ui.menuBar.tabCloseRequested.connect(self.close_tab)

        # connect or disconnect
        self.ui.connect.clicked.connect(self.connect)
        self.ui.disconnect.clicked.connect(self.disconnect)

        # Mapping of Signals and Slots
        self.ui.clear_logs.clicked.connect(lambda: self.ui.out_put.clear())
        self.ui.cmd_input.returnPressed.connect(self.cmd_input)
        self.ui.cmd_send.clicked.connect(self.cmd_input)

        # Enum data
        self._qt_key = {
            '0x1000000': 'Key_Escape',
            '0x1000001': 'Key_Tab',
            '0x1000002': 'Key_Backtab',
            '0x1000003': 'Key_Backspace',
            '0x1000004': 'Key_Return',
            '0x1000005': 'Key_Enter',
            '0x1000006': 'Key_Insert',
            '0x1000007': 'Key_Delete',
            '0x1000008': 'Key_Pause',
            '0x1000009': 'Key_Print',
            '0x100000a': 'Key_SysReq',
            '0x100000b': 'Key_Clear',
            '0x1000010': 'Key_Home',
            '0x1000011': 'Key_End',
            '0x1000012': 'Key_Left',
            '0x1000013': 'Key_Up',
            '0x1000014': 'Key_Right',
            '0x1000015': 'Key_Down',
            '0x1000016': 'Key_PageUp',
            '0x1000017': 'Key_PageDown',
            '0x1000020': 'Key_Shift',
            '0x1000021': 'Key_Control',
            '0x1000022': 'Key_Meta',
            '0x1000023': 'Key_Alt',
            '0x1001103': 'Key_AltGr',
            '0x1000024': 'Key_CapsLock',
            '0x1000025': 'Key_NumLock',
            '0x1000026': 'Key_ScrollLock',
            '0x1000030': 'Key_F1',
            '0x1000031': 'Key_F2',
            '0x1000032': 'Key_F3',
            '0x1000033': 'Key_F4',
            '0x1000034': 'Key_F5',
            '0x1000035': 'Key_F6',
            '0x1000036': 'Key_F7',
            '0x1000037': 'Key_F8',
            '0x1000038': 'Key_F9',
            '0x1000039': 'Key_F10',
            '0x100003a': 'Key_F11',
            '0x100003b': 'Key_F12',
            '0x100003c': 'Key_F13',
            '0x100003d': 'Key_F14',
            '0x100003e': 'Key_F15',
            '0x100003f': 'Key_F16',
            '0x1000040': 'Key_F17',
            '0x1000041': 'Key_F18',
            '0x1000042': 'Key_F19',
            '0x1000043': 'Key_F20',
            '0x1000044': 'Key_F21',
            '0x1000045': 'Key_F22',
            '0x1000046': 'Key_F23',
            '0x1000047': 'Key_F24',
            '0x1000048': 'Key_F25',
            '0x1000049': 'Key_F26',
            '0x100004a': 'Key_F27',
            '0x100004b': 'Key_F28',
            '0x100004c': 'Key_F29',
            '0x100004d': 'Key_F30',
            '0x100004e': 'Key_F31',
            '0x100004f': 'Key_F32',
            '0x1000050': 'Key_F33',
            '0x1000051': 'Key_F34',
            '0x1000052': 'Key_F35',
            '0x1000053': 'Key_Super_L',
            '0x1000054': 'Key_Super_R',
            '0x1000055': 'Key_Menu',
            '0x1000056': 'Key_Hyper_L',
            '0x1000057': 'Key_Hyper_R',
            '0x1000058': 'Key_Help',
            '0x1000059': 'Key_Direction_L',
            '0x1000060': 'Key_Direction_R',
            '0x20': 'Key_Space',
            '0x21': 'Key_Exclam',
            '0x22': 'Key_QuoteDbl',
            '0x23': 'Key_NumberSign',
            '0x24': 'Key_Dollar',
            '0x25': 'Key_Percent',
            '0x26': 'Key_Ampersand',
            '0x27': 'Key_Apostrophe',
            '0x28': 'Key_ParenLeft',
            '0x29': 'Key_ParenRight',
            '0x2a': 'Key_Asterisk',
            '0x2b': 'Key_Plus',
            '0x2c': 'Key_Comma',
            '0x2d': 'Key_Minus',
            '0x2e': 'Key_Period',
            '0x2f': 'Key_Slash',
            '0x30': 'Key_0',
            '0x31': 'Key_1',
            '0x32': 'Key_2',
            '0x33': 'Key_3',
            '0x34': 'Key_4',
            '0x35': 'Key_5',
            '0x36': 'Key_6',
            '0x37': 'Key_7',
            '0x38': 'Key_8',
            '0x39': 'Key_9',
            '0x3a': 'Key_Colon',
            '0x3b': 'Key_Semicolon',
            '0x3c': 'Key_Less',
            '0x3d': 'Key_Equal',
            '0x3e': 'Key_Greater',
            '0x3f': 'Key_Question',
            '0x40': 'Key_At',
            '0x41': 'Key_A',
            '0x42': 'Key_B',
            '0x43': 'Key_C',
            '0x44': 'Key_D',
            '0x45': 'Key_E',
            '0x46': 'Key_F',
            '0x47': 'Key_G',
            '0x48': 'Key_H',
            '0x49': 'Key_I',
            '0x4a': 'Key_J',
            '0x4b': 'Key_K',
            '0x4c': 'Key_L',
            '0x4d': 'Key_M',
            '0x4e': 'Key_N',
            '0x4f': 'Key_O',
            '0x50': 'Key_P',
            '0x51': 'Key_Q',
            '0x52': 'Key_R',
            '0x53': 'Key_S',
            '0x54': 'Key_T',
            '0x55': 'Key_U',
            '0x56': 'Key_V',
            '0x57': 'Key_W',
            '0x58': 'Key_X',
            '0x59': 'Key_Y',
            '0x5a': 'Key_Z',
            '0x5b': 'Key_{',
            '0x5c': 'Key_|',
            '0x5d': 'Key_}',
            '0x60': 'Key_~',
        }
        self.history_cmds = ['0_0_0' for _ in range(15)]  # history_commands_max = 15
        self.cmd_id = 14
        self.ui.out_put.verticalScrollBar().setMaximum(1024)

    def connect(self):
        _com_ports, _band_rate = self.ui.ser_ports_list.currentText(), int(self.ui.band_rate.currentText())
        _data_bit, _parity_bit = self.ui.data_bit.currentText(), self.ui.parity_bit.currentText()
        _stop_bit = self.ui.stop_bit.currentText()
        _rts_cts, _xon_xoff, _dtr_dsr = False, False, False
        # data cleaning
        _target_port = re.findall('.*\((.*)\)', _com_ports)
        if len(_target_port) == 0:
            return
        if self.ui.rts_cts.isChecked():
            _rts_cts = True
        if self.ui.xon_xoff.isChecked():
            _xon_xoff = True
        if self.ui.dtr_dsr.isChecked():
            _dtr_dsr = True
        _byte_size = {
            'EIGHTBITS': serial.EIGHTBITS,
            'SEVENBITS': serial.SEVENBITS,
            'SIXBITS': serial.SIXBITS,
            'FIVEBITS': serial.FIVEBITS
        }
        _stop_bits = {
            'STOPBITS_ONE': serial.STOPBITS_ONE,
            'STOPBITS_POINT_FIVE': serial.STOPBITS_ONE_POINT_FIVE,
            'STOPBITS_TWO': serial.STOPBITS_TWO
        }
        _parities = {
            'PARITY_NONE': serial.PARITY_NONE,
            'PARITY_EVEN': serial.PARITY_EVEN,
            'PARITY_ODD': serial.PARITY_ODD,
            'PARITY_MARK': serial.PARITY_MARK,
            'PARITY_SPACE': serial.PARITY_SPACE
        }
        try:
            """
            (port=None, baudrate=9600, bytesize=EIGHTBITS, parity=PARITY_NONE, stopbits=STOPBITS_ONE, timeout=None, 
            xonxoff=False, rtscts=False, write_timeout=None, dsrdtr=False, inter_byte_timeout=None, exclusive=None)
            """
            self.ser = serial.Serial(port=_target_port[0], baudrate=_band_rate, bytesize=_byte_size[_data_bit],
                                     stopbits=_stop_bits[_stop_bit], parity=_parities[_parity_bit], rtscts=_rts_cts,
                                     xonxoff=_xon_xoff, dsrdtr=_dtr_dsr, timeout=0.1)
            self.ui.out_put.verticalScrollBar().maximum()
            if self.ser.isOpen():
                self.ui.statusbar.showMessage(f'{_target_port[0]} Connected')
                self.ser.ser_flag = True
                self.ui.connect.setEnabled(False)
                time.sleep(0.02)
                self.ui.connect.setIcon(QIcon(r'.\images\widgets\connect_gray.png'))
                self.ui.connect.setIconSize(QSize(91, 18))
                time.sleep(0.02)
                self.ui.disconnect.setEnabled(True)
                self.ui.disconnect.setIcon(QIcon(r'.\images\widgets\disconnect.png'))
                self.ui.disconnect.setIconSize(QSize(91, 18))
                time.sleep(0.02)
                self.ui.log_box.setTitle(f'Serial-{_target_port[0]}')
                time.sleep(0.02)
                self.ser_recv = MessageRecv(ui_instance=self.ui, ser_instance=self.ser)
                self.ser_recv.daemon = True
                self.ser_recv.homer.connect(self.dovecote)
                self.ser_recv.start()
                self.ser.reset_input_buffer()
                self.ser.reset_output_buffer()
                cmd = bytes('\r\n'.encode('ascii'))
                self.ser.write(cmd)
                self.ui.cmd_input.setFocus()
            else:
                self.ser.close()
                return
        except Exception as E:
            self.ui.out_put.append(f'\n{str(E)}')

    def cmd_input(self):
        if not self.ser:
            return
        try:
            _text = self.ui.cmd_input.text()
            _cmd = _text + '\r\n'
            self.ser.write(bytes(_cmd.encode('ascii')))
            self.ser.flush()
            self.ui.cmd_input.clear()
            self.ui.cmd_input.setFocus()
            if _text == '':
                return
            self.history_cmds.append(_text.replace('\r\n', '').replace('\n', ''))
            self.history_cmds.pop(0)
        except Exception as E:
            traceback.print_exc()
            self.disconnect()

    def cmd_send(self, cmd):
        if not self.ser:
            self.ui.out_put.append('未检测到活动的串口实例')
            return
        _cmd = cmd + '\r\n'
        self.ser.write(bytes(_cmd.encode('ascii')))
        self.ser.flush()

    def disconnect(self):
        self.ser.close()
        self.ser = None
        self.ser_flag = False
        self.ui.statusbar.showMessage('Here we goooo!')
        self.ui.connect.setEnabled(True)
        self.ui.connect.setIcon(QIcon(r'.\images\widgets\connect.png'))
        self.ui.connect.setIconSize(QSize(91, 18))
        self.ui.disconnect.setEnabled(False)
        self.ui.disconnect.setIcon(QIcon(r'.\images\widgets\disconnect_gray.png'))
        self.ui.disconnect.setIconSize(QSize(91, 18))
        self.ui.log_box.setTitle(f'Serial-X-Dead')

    def keyPressEvent(self, event):
        # TODO
        _key = hex(event.key())
        if _key in self._qt_key.keys():
            if _key == '0x1000004' or _key == '0x1000005':  # ENTER, commit input command
                if self.ui.cmd_input.hasFocus():
                    self.cmd_input()
            if _key == '0x1000013':  # Key_Up pressed, display history command
                if self.cmd_id > 0:
                    self.cmd_id -= 1
                else:
                    self.cmd_id = 0
                if self.history_cmds[self.cmd_id] == '0_0_0':
                    return
                self.ui.cmd_input.setText(self.history_cmds[self.cmd_id])

            if _key == '0x1000015':  # Key_Down pressed, display history command
                if self.cmd_id < 14:
                    self.cmd_id += 1
                else:
                    self.cmd_id = 14
                if self.history_cmds[self.cmd_id] == '0_0_0':
                    return
                self.ui.cmd_input.setText(self.history_cmds[self.cmd_id])

    def dovecote(self, msg):
        # all the request of Ui widget operation should be delivered to this method in a queue
        # format of msg: (widget, 'operation', payload)
        # example:
        #       you want addItems to a QComboBox：
        #           [msg[0].addItems(port for port in msg[2])]
        match msg[1]:
            # QComboBox Operations
            case 'QComboBox.addItems':  # QComboBox addItems
                [msg[0].addItems(port for port in msg[2])]
            case 'QComboBox.addItem':  # QComboBox addItem
                msg[0].addItem(msg[2])
                msg[0].setCurrentText(msg[2])
            case 'QComboBox.clear':  # QComboBox clear
                msg[0].clear()
            case 'QComboBox.setCurrentText':
                msg[0].setCurrentText(msg[2])
            # QTextEdit Operation
            case 'QTextEdit.append':
                msg[0].append(msg[2])
                QApplication.processEvents()
                msg[0].verticalScrollBar().setValue(msg[0].verticalScrollBar().maximum())
            # QLineEdit Operations
            case 'QLineEdit.setText':  # QLineEdit.setText()
                msg[0].setText(msg[2])
            # QButton Operations
            case 'QButton.click':  # QButton.click()
                msg[0].click()
            # QTextBrowser Operations
            case 'QTextBrowser.append':
                msg[0].append(msg[2])
                QApplication.processEvents()
                msg[0].verticalScrollBar().setValue(msg[0].verticalScrollBar().maximum())
                QApplication.processEvents()
            case _:
                self.ui.out_put.append(str(msg))

    def new_tab(self):
        # add new Tab page to self.ui.menuBar
        self.new_tab_dia.close()
        QApplication.processEvents()
        _tab_name = self.new_tab_dia.tab_name()
        if _tab_name == '':
            self.ui.out_put.append('Tab name should not be Empty')
            return
        _sec = _tab_name
        # If _sec not in ini.sections, add new tab, else return
        with open('./cfg/clb_list.ini', 'r') as cfg_r:
            self.clb_config.read(cfg_r)
            if self.clb_config.has_section(_sec):
                self.ui.out_put.append(f'Tab name {_sec} already exists')
                cfg_r.close()
                return
            with open('./cfg/clb_list.ini', 'w') as cfg_w:
                # write 时会讲read读取内容与add_section内容一同写入，因此应采用w方式open，写入前清空文件内容，a方式open会出现重复条目
                self.clb_config.add_section(_sec)
                self.clb_config.write(cfg_w)
        _new_tab = QWidget()
        self.ui.menuBar.addTab(_new_tab, _tab_name)
        self.ui.menuBar.setCurrentIndex(self.ui.menuBar.indexOf(_new_tab))  # 设定 focus到新加Tab
        # Each Tab need a qhbox layout to contain NewWidgets, layout_id map to QTabWidget.CurrentIndex()
        _tab_layout = self.qhbox[self.ui.menuBar.indexOf(_new_tab)]
        _tab_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)  # 左对齐
        _new_tab.setLayout(_tab_layout)

    def new_clb(self):
        # add new QCommandLinkButton to Tabs of self.ui.menuBar
        self.new_bt_dia.close()
        QApplication.processEvents()
        _sec = self.ui.menuBar.tabText(self.current_index)
        _info = self.new_bt_dia.info()
        with open('./cfg/clb_list.ini', 'r') as cfg_r:
            self.clb_config.read(cfg_r)
            cfg_r.close()
            if _info[0] in self.clb_config.options(_sec):
                print(self.clb_config.options(_sec))
                self.ui.out_put.append('Target already exists')
                return
        with open('./cfg/clb_list.ini', 'w') as cfg_w:
            self.clb_config.set(_sec, _info[0], _info[1])
            self.clb_config.write(cfg_w)
            cfg_w.close()
        _clb = QCommandLinkButton()
        _clb.setText(_info[0])
        _stretch = len(_info[0])
        _width = _stretch * 12 + 10
        _clb.setDescription(_info[1])
        _clb.setIconSize(QSize(1, 1))
        _clb.resize(QSize(_width, 30))
        _clb.setMaximumSize(QSize(_width, 30))
        _clb.setObjectName(_info[0])
        self.qhbox[self.current_index].addWidget(_clb, alignment=Qt.AlignmentFlag.AlignLeft)  # 左对齐
        _clb.clicked.connect(lambda: self.cmd_send(_info[1]))

    def remove_clb_tab(self, target):
        _type = ['tab', 'clb']
        if target not in _type:
            return
        _sec, _option = self.ui.menuBar.tabText(self.current_index), self.current_widget.objectName()
        # modify clb_list.ini
        with open('./cfg/clb_list.ini', 'r') as cfg_r:
            self.clb_config.read(cfg_r)
            cfg_r.close()
            with open('./cfg/clb_list.ini', 'w') as cfg_w:
                match target:
                    case 'tab':
                        if self.clb_config.has_section(_sec):
                            self.clb_config.remove_section(_sec)
                            self.ui.menuBar.removeTab(self.current_index)
                            self.ui.cmd_input.setFocus()  # 切换焦点触发ui刷新
                            QApplication.processEvents()
                            self.clb_config.write(cfg_w)
                            cfg_w.close()
                    case 'clb':
                        if self.clb_config.has_option(_sec, _option):
                            print(f'remove_widget: {self.current_widget.objectName}')
                            self.qhbox[self.current_index].removeWidget(self.current_widget)
                            self.ui.cmd_input.setFocus()  # 切换焦点触发ui刷新
                            QApplication.processEvents()
                            self.clb_config.remove_option(_sec, _option)
                            self.clb_config.write(cfg_w)
                            cfg_w.close()
                    case _:
                        QApplication.processEvents()
                        pass

    def load_clb_cfg(self):
        self.ui.menuBar.clear()
        QApplication.processEvents()
        self.clb_config.read('./cfg/clb_list.ini')
        _sections = self.clb_config.sections()
        _id = 0
        for _sec in _sections:
            _newTab = QWidget()
            self.ui.menuBar.addTab(_newTab, _sec)  # addTab
            _bt_layout = self.qhbox[_id]
            _bt_layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
            _bt_layout.setObjectName(f"bt_layout_{str(_id)}")
            self.ui.menuBar.setCurrentIndex(_id)
            _newTab.setLayout(_bt_layout)

            for _opt in self.clb_config.options(_sec):
                _clb = QCommandLinkButton()
                _clb.setText(_opt)
                _stretch = len(_opt)
                _width = _stretch * 12 + 10
                _clb.setDescription(self.clb_config.get(_sec, _opt))
                _clb.setIconSize(QSize(1, 1))
                _clb.resize(QSize(_width, 35))
                _clb.setMaximumSize(QSize(_width, 35))
                _clb.setObjectName(_opt)
                _bt_layout.addWidget(_clb, alignment=Qt.AlignmentFlag.AlignLeft)  # Widgets左对齐
                _clb.clicked.connect(lambda: self.cmd_send(_clb.description()))
                _clb.customContextMenuRequested.connect(lambda: self.show_context_menu())
            _id += 1

        self.ui.menuBar.setCurrentIndex(self.current_index)

    def close_tab(self, index):
        # tabCloseRequested signal has index attribute
        self.ui.menuBar.removeTab(index)

    def show_context_menu(self):
        # Discard ,  信号注册要放到init中，否则要第一次点击无效，需要二次点击action才能触发信号
        # CommandLinkButton 右击显示 AddNewTab/RemoveCurrentTab/AddNewCommandLinkButton/RemoveCurrentCommandLinkButton/..
        self.context_menu.exec(QCursor.pos())
        # TODO , which item to delete
        self.add_tab_act.triggered.connect(lambda: print('add_tab_action triggered'))
        self.add_tab_act.triggered.connect(lambda: self.new_tab_dia.exec())
        self.add_clb_act.triggered.connect(lambda: print('add_clb_act triggered'))
        self.add_clb_act.triggered.connect(lambda: self.new_bt_dia.exec())

    def get_focus_widget(self):
        self.current_widget = QApplication.focusWidget()
        if self.current_widget:
            # print(self.current_widget.objectName())
            pass

    def get_tab_id(self):
        # 初始化阶段currentIndex = -1
        _id = self.ui.menuBar.currentIndex()
        if _id == -1:
            _id = 0
        self.current_index = _id

    def toolbar_action(self):
        pass


class SerialPortScan(QThread):
    """
    自动扫描串口设备，结果发送给UI主线程显示到QComboBox对象self.ui.ser_ports_list
    """
    homer = pyqtSignal(tuple)

    def __init__(self, ui_instance):
        super(SerialPortScan, self).__init__()
        self.ui = ui_instance

    def run(self):
        self.homer.emit((self.ui.ser_ports_list, 'QComboBox.clear', ''))
        _ser_list = []
        _vid, _pid = '10C4', 'EA60'  # 可根据vid/pid对已知串口设备做过滤
        _serial_ports = list(serial.tools.list_ports.comports())
        for item in _serial_ports:
            if '蓝牙链接上的标准串行' not in item.description:
                _ser_list.append(item.description)
        #[_ser_list.append(item.description) for item in _serial_ports]
        """
        for item in _serial_ports:
            if item.vid and item.pid:
                if f'{item.vid:0>4x}'.upper() == _vid and f'{item.pid:0>4x}'.upper() == _pid:
                    _ncp_list.append(item.description) 
                    """
        self.homer.emit((self.ui.ser_ports_list, 'QComboBox.addItems', _ser_list))
        time.sleep(0.5)


class MessageRecv(QThread):

    homer = pyqtSignal(tuple)

    def __init__(self, ui_instance, ser_instance):
        super(MessageRecv, self).__init__()
        self.ui = ui_instance
        self.ser = ser_instance

    def run(self):
        while 1:
            try:
                _line = self.ser.readall().decode('utf-8').replace('\n', '')
                if len(_line) > 0:   # 调试用的设备串口回显默认带前缀 '-->', 故过滤
                    self.homer.emit((self.ui.out_put, 'QTextBrowser.append', _line))
                    self.ser.flushOutput()
                    self.ser.flushInput()
            except Exception:
                return


class NewTab(QDialog, Ui_new_tab_dia):
    def __init__(self):
        super(NewTab, self).__init__()
        self.setupUi(self)

    def tab_name(self):
        _tab_name = self.new_tab_name.text()
        return _tab_name


class NewBtDialog(QDialog, Ui_new_cmd_button):
    def __init__(self):
        super(NewBtDialog, self).__init__()
        self.setupUi(self)

    def info(self):
        _cmd_name = self.cmd_name.text()
        _cmd = self.cmd_payload.text()
        return (_cmd_name, _cmd)


if __name__ == "__main__":
    import sys
    app = QApplication(sys.argv)
    app.processEvents()
    ui = ComController()
    ui.setWindowIcon(QIcon(".\images\widgets\com_port.png"))
    ui.show()
    app.exec()
